1 /** 2 Compile-time reflection to find unit tests and set their properties. 3 */ 4 module unit_threaded.reflection; 5 6 import unit_threaded.from; 7 8 /* 9 These standard library imports contain something important for the code below. 10 Unfortunately I don't know what they are so they're to prevent breakage. 11 */ 12 import std.traits; 13 import std.algorithm; 14 import std.array; 15 16 /// 17 alias void delegate() TestFunction; 18 19 /** 20 * Common data for test functions and test classes 21 */ 22 struct TestData { 23 string name; 24 TestFunction testFunction; ///only used for functions, null for classes 25 bool hidden; 26 bool shouldFail; 27 bool singleThreaded; 28 bool builtin; 29 string suffix; // append to end of getPath 30 string[] tags; 31 TypeInfo exceptionTypeInfo; // for ShouldFailWith 32 int flakyRetries = 0; 33 34 /// The test's name 35 string getPath() const pure nothrow { 36 string path = name.dup; 37 import std.array : empty; 38 39 if (!suffix.empty) 40 path ~= "." ~ suffix; 41 return path; 42 } 43 44 /// If the test is a class 45 bool isTestClass() @safe const pure nothrow { 46 return testFunction is null; 47 } 48 } 49 50 /** 51 * Finds all test cases (functions, classes, built-in unittest blocks) 52 * Template parameters are module strings 53 */ 54 const(TestData)[] allTestData(MOD_STRINGS...)() 55 if (from!"std.meta".allSatisfy!(from!"std.traits".isSomeString, typeof(MOD_STRINGS))) { 56 import std.array : join; 57 import std.range : iota; 58 import std.format : format; 59 import std.algorithm : map; 60 61 string getModulesString() { 62 string[] modules; 63 foreach (i, module_; MOD_STRINGS) 64 modules ~= "module%d = %s".format(i, module_); 65 return modules.join(", "); 66 } 67 68 enum modulesString = getModulesString; 69 mixin("import " ~ modulesString ~ ";"); 70 mixin("return allTestData!(" ~ MOD_STRINGS.length.iota.map!(i => "module%d".format(i)) 71 .join(", ") ~ ");"); 72 } 73 74 /** 75 * Finds all test cases (functions, classes, built-in unittest blocks) 76 * Template parameters are module symbols 77 */ 78 const(TestData)[] allTestData(MOD_SYMBOLS...)() 79 if (!from!"std.meta".anySatisfy!(from!"std.traits".isSomeString, typeof(MOD_SYMBOLS))) { 80 auto allTestsWithFunc(string expr)() pure { 81 import std.traits : ReturnType; 82 import std.meta : AliasSeq; 83 84 //tests is whatever type expr returns 85 ReturnType!(mixin(expr ~ q{!(MOD_SYMBOLS[0])})) tests; 86 foreach (module_; AliasSeq!MOD_SYMBOLS) { 87 tests ~= mixin(expr ~ q{!module_()}); //e.g. tests ~= moduleTestClasses!module_ 88 } 89 return tests; 90 } 91 92 return allTestsWithFunc!"moduleTestClasses" 93 ~ allTestsWithFunc!"moduleTestFunctions" ~ allTestsWithFunc!"moduleUnitTests"; 94 } 95 96 private template Identity(T...) if (T.length > 0) { 97 static if (__traits(compiles, { alias x = T[0]; })) 98 alias Identity = T[0]; 99 else 100 enum Identity = T[0]; 101 } 102 103 /** 104 * Finds all built-in unittest blocks in the given module. 105 * Recurses into structs, classes, and unions of the module. 106 * 107 * @return An array of TestData structs 108 */ 109 TestData[] moduleUnitTests(alias module_)() pure nothrow { 110 111 // Return a name for a unittest block. If no @Name UDA is found a name is 112 // created automatically, else the UDA is used. 113 // the weird name for the first template parameter is so that it doesn't clash 114 // with a package name 115 string unittestName(alias _theUnitTest, int index)() @safe nothrow { 116 import std.conv : text, to; 117 import std.traits : fullyQualifiedName; 118 import std.traits : getUDAs; 119 import std.meta : Filter; 120 import unit_threaded.attrs : Name; 121 122 mixin("import " ~ fullyQualifiedName!module_ ~ ";"); //so it's visible 123 124 enum nameAttrs = getUDAs!(_theUnitTest, Name); 125 static assert(nameAttrs.length == 0 || nameAttrs.length == 1, 126 "Found multiple Name UDAs on unittest"); 127 128 enum strAttrs = Filter!(isStringUDA, __traits(getAttributes, _theUnitTest)); 129 enum hasName = nameAttrs.length || strAttrs.length == 1; 130 enum prefix = fullyQualifiedName!(__traits(parent, _theUnitTest)) ~ "."; 131 132 static if (hasName) { 133 static if (nameAttrs.length == 1) 134 return prefix ~ nameAttrs[0].value; 135 else 136 return prefix ~ strAttrs[0]; 137 } else { 138 string name; 139 try { 140 return prefix ~ "unittest" ~ (index).to!string; 141 } catch (Exception) { 142 assert(false, text("Error converting ", index, " to string")); 143 } 144 } 145 } 146 147 void function() getUDAFunction(alias composite, alias uda)() pure nothrow { 148 import std.traits : fullyQualifiedName, moduleName, isSomeFunction, 149 hasUDA; 150 151 // Due to: 152 // https://issues.dlang.org/show_bug.cgi?id=17441 153 // moduleName!composite might fail, so we try to import that only if 154 // if compiles, then try again with fullyQualifiedName 155 enum moduleNameStr = `import ` ~ moduleName!composite ~ `;`; 156 enum fullyQualifiedStr = `import ` ~ fullyQualifiedName!composite ~ `;`; 157 158 static if (__traits(compiles, mixin(moduleNameStr))) 159 mixin(moduleNameStr); 160 else static if (__traits(compiles, mixin(fullyQualifiedStr))) 161 mixin(fullyQualifiedStr); 162 163 void function()[] ret; 164 foreach (memberStr; __traits(allMembers, composite)) { 165 static if (__traits(compiles, Identity!(__traits(getMember, composite, memberStr)))) { 166 alias member = Identity!(__traits(getMember, composite, memberStr)); 167 static if (__traits(compiles, &member)) { 168 static if (isSomeFunction!member && hasUDA!(member, uda)) { 169 ret ~= &member; 170 } 171 } 172 } 173 } 174 175 return ret.length ? ret[0] : null; 176 } 177 178 TestData[] testData; 179 180 void addMemberUnittests(alias composite)() pure nothrow { 181 182 import unit_threaded.attrs; 183 import unit_threaded.uda : hasUtUDA; 184 import std.traits : hasUDA; 185 import std.meta : Filter, aliasSeqOf; 186 import std.algorithm : map, cartesianProduct; 187 188 foreach (index, eLtEstO; __traits(getUnitTests, composite)) { 189 190 enum dontTest = hasUDA!(eLtEstO, DontTest); 191 192 static if (!dontTest) { 193 194 enum name = unittestName!(eLtEstO, index); 195 enum hidden = hasUDA!(eLtEstO, HiddenTest); 196 enum shouldFail = hasUDA!(eLtEstO, ShouldFail) 197 || hasUtUDA!(eLtEstO, ShouldFailWith); 198 enum singleThreaded = hasUDA!(eLtEstO, Serial); 199 enum builtin = true; 200 enum suffix = ""; 201 202 // let's check for @Values UDAs, which are actually of type ValuesImpl 203 enum isValues(alias T) = is(typeof(T)) && is(typeof(T) : ValuesImpl!U, U); 204 alias valuesUDAs = Filter!(isValues, __traits(getAttributes, eLtEstO)); 205 206 enum isTags(alias T) = is(typeof(T)) && is(typeof(T) == Tags); 207 enum tags = tagsFromAttrs!(Filter!(isTags, __traits(getAttributes, eLtEstO))); 208 enum exceptionTypeInfo = getExceptionTypeInfo!eLtEstO; 209 enum flakyRetries = getFlakyRetries!(eLtEstO); 210 211 static if (valuesUDAs.length == 0) { 212 testData ~= TestData(name, () { 213 auto setup = getUDAFunction!(composite, Setup); 214 auto shutdown = getUDAFunction!(composite, Shutdown); 215 216 if (setup) 217 setup(); 218 scope (exit) 219 if (shutdown) 220 shutdown(); 221 222 eLtEstO(); 223 }, hidden, shouldFail, singleThreaded, builtin, suffix, 224 tags, exceptionTypeInfo, flakyRetries); 225 } else { 226 import std.range; 227 228 // cartesianProduct doesn't work with only one range, so in the usual case 229 // of only one @Values UDA, we bind to prod with a range of tuples, just 230 // as returned by cartesianProduct. 231 232 static if (valuesUDAs.length == 1) { 233 import std.typecons; 234 235 enum prod = valuesUDAs[0].values.map!(a => tuple(a)); 236 } else { 237 mixin(`enum prod = cartesianProduct(` ~ valuesUDAs.length.iota.map!( 238 a => `valuesUDAs[` ~ guaranteedToString(a) ~ `].values`).join( 239 ", ") ~ `);`); 240 } 241 242 foreach (comb; aliasSeqOf!prod) { 243 enum valuesName = valuesName(comb); 244 245 static if (hasUDA!(eLtEstO, AutoTags)) 246 enum realTags = tags ~ valuesName.split(".").array; 247 else 248 enum realTags = tags; 249 250 testData ~= TestData(name ~ "." ~ valuesName, () { 251 foreach (i; aliasSeqOf!(comb.length.iota)) 252 ValueHolder!(typeof(comb[i])).values[i] = comb[i]; 253 eLtEstO(); 254 }, hidden, shouldFail, singleThreaded, builtin, suffix, 255 realTags, exceptionTypeInfo, flakyRetries); 256 } 257 } 258 } 259 } 260 } 261 262 // Keeps track of mangled names of everything visited. 263 bool[string] visitedMembers; 264 265 void addUnitTestsRecursively(alias composite)() pure nothrow { 266 import std.traits : fullyQualifiedName; 267 268 mixin("import " ~ fullyQualifiedName!module_ ~ ";"); //so it's visible 269 270 if (composite.mangleof in visitedMembers) 271 return; 272 visitedMembers[composite.mangleof] = true; 273 addMemberUnittests!composite(); 274 foreach (member; __traits(allMembers, composite)) { 275 enum notPrivate = __traits(compiles, mixin(member)); //only way I know to check if private 276 static if (notPrivate && // If visibility of the member is deprecated, the next line still returns true 277 // and yet spills deprecation warning. If deprecation is turned into error, 278 // all works as intended. 279 __traits(compiles, __traits(getMember, composite, 280 member)) && __traits(compiles, __traits(allMembers, __traits(getMember, composite, member))) 281 && __traits(compiles, recurse!(__traits(getMember, composite, member)))) { 282 recurse!(__traits(getMember, composite, member)); 283 } 284 } 285 } 286 287 void recurse(child)() pure nothrow { 288 enum notPrivate = __traits(compiles, child.init); //only way I know to check if private 289 static if (is(child == class) || is(child == struct) || is(child == union)) { 290 addUnitTestsRecursively!child; 291 } 292 } 293 294 addUnitTestsRecursively!module_(); 295 return testData; 296 } 297 298 private TypeInfo getExceptionTypeInfo(alias Test)() { 299 import unit_threaded.uda : hasUtUDA, getUtUDAs; 300 import unit_threaded.attrs : ShouldFailWith; 301 302 static if (hasUtUDA!(Test, ShouldFailWith)) { 303 alias uda = getUtUDAs!(Test, ShouldFailWith)[0]; 304 return typeid(uda.Type); 305 } else 306 return null; 307 } 308 309 private string valuesName(T)(T tuple) { 310 import std.range : iota; 311 import std.meta : aliasSeqOf; 312 import std.array : join; 313 314 string[] parts; 315 foreach (a; aliasSeqOf!(tuple.length.iota)) 316 parts ~= guaranteedToString(tuple[a]); 317 return parts.join("."); 318 } 319 320 private string guaranteedToString(T)(T value) nothrow pure @safe { 321 import std.conv; 322 323 try 324 return value.to!string; 325 catch (Exception ex) 326 assert(0, "Could not convert value to string"); 327 } 328 329 private string getValueAsString(T)(T value) nothrow pure @safe { 330 import std.conv; 331 332 try 333 return value.to!string; 334 catch (Exception ex) 335 assert(0, "Could not convert value to string"); 336 } 337 338 private template isStringUDA(alias T) { 339 import std.traits : isSomeString; 340 341 static if (__traits(compiles, isSomeString!(typeof(T)))) 342 enum isStringUDA = isSomeString!(typeof(T)); 343 else 344 enum isStringUDA = false; 345 } 346 347 @safe pure unittest { 348 static assert(isStringUDA!"foo"); 349 static assert(!isStringUDA!5); 350 } 351 352 private template isPrivate(alias module_, string moduleMember) { 353 import unit_threaded.uda : HasTypes; 354 355 alias ut_mmbr__ = Identity!(__traits(getMember, module_, moduleMember)); 356 357 static if (__traits(compiles, isSomeFunction!(ut_mmbr__))) { 358 static if (__traits(compiles, &ut_mmbr__)) 359 enum isPrivate = false; 360 else static if (__traits(compiles, new ut_mmbr__)) 361 enum isPrivate = false; 362 else static if (__traits(compiles, HasTypes!ut_mmbr__)) 363 enum isPrivate = !HasTypes!ut_mmbr__; 364 else 365 enum isPrivate = true; 366 } else { 367 enum isPrivate = true; 368 } 369 } 370 371 // if this member is a test function or class, given the predicate 372 private template PassesTestPred(alias module_, alias pred, string moduleMember) { 373 import std.traits : fullyQualifiedName; 374 import unit_threaded.meta : importMember; 375 import unit_threaded.uda : HasAttribute; 376 import unit_threaded.attrs : DontTest; 377 378 //should be the line below instead but a compiler bug prevents it 379 //mixin(importMember!module_(moduleMember)); 380 mixin("import " ~ fullyQualifiedName!module_ ~ ";"); 381 alias I(T...) = T; 382 static if (!__traits(compiles, I!(__traits(getMember, module_, moduleMember)))) { 383 enum PassesTestPred = false; 384 } else { 385 alias member = I!(__traits(getMember, module_, moduleMember)); 386 387 template canCheckIfSomeFunction(T...) { 388 enum canCheckIfSomeFunction = T.length == 1 && __traits(compiles, 389 isSomeFunction!(T[0])); 390 } 391 392 private string funcCallMixin(alias T)() { 393 import std.conv : to; 394 395 string[] args; 396 foreach (i, ParamType; Parameters!T) { 397 args ~= `arg` ~ i.to!string; 398 } 399 400 return moduleMember ~ `(` ~ args.join(`,`) ~ `);`; 401 } 402 403 private string argsMixin(alias T)() { 404 import std.conv : to; 405 406 string[] args; 407 foreach (i, ParamType; Parameters!T) { 408 args ~= ParamType.stringof ~ ` arg` ~ i.to!string ~ `;`; 409 } 410 411 return args.join("\n"); 412 } 413 414 template canCallMember() { 415 void _f() { 416 mixin(argsMixin!member); 417 mixin(funcCallMixin!member); 418 } 419 } 420 421 template canInstantiate() { 422 void _f() { 423 mixin(`auto _ = new ` ~ moduleMember ~ `;`); 424 } 425 } 426 427 template isPrivate() { 428 static if (!canCheckIfSomeFunction!member) { 429 enum isPrivate = !__traits(compiles, __traits(getMember, module_, moduleMember)); 430 } else { 431 static if (isSomeFunction!member) { 432 enum isPrivate = !__traits(compiles, canCallMember!()); 433 } else static if (is(member)) { 434 static if (isAggregateType!member) 435 enum isPrivate = !__traits(compiles, canInstantiate!()); 436 else 437 enum isPrivate = !__traits(compiles, 438 __traits(getMember, module_, moduleMember)); 439 } else { 440 enum isPrivate = !__traits(compiles, __traits(getMember, 441 module_, moduleMember)); 442 } 443 } 444 } 445 446 enum notPrivate = !isPrivate!(); 447 enum PassesTestPred = !isPrivate!() && pred!(module_, moduleMember) 448 && !HasAttribute!(module_, moduleMember, DontTest); 449 } 450 } 451 452 /** 453 * Finds all test classes (classes implementing a test() function) 454 * in the given module 455 */ 456 TestData[] moduleTestClasses(alias module_)() pure nothrow { 457 458 template isTestClass(alias module_, string moduleMember) { 459 import unit_threaded.meta : importMember; 460 import unit_threaded.uda : HasAttribute; 461 import unit_threaded.attrs : UnitTest; 462 import std.traits : isAggregateType; 463 464 alias member = Identity!(__traits(getMember, module_, moduleMember)); 465 466 static if (.isPrivate!(module_, moduleMember)) { 467 enum isTestClass = false; 468 } else static if (!__traits(compiles, isAggregateType!(member))) { 469 enum isTestClass = false; 470 } else static if (!isAggregateType!(member)) { 471 enum isTestClass = false; 472 } else static if (!__traits(compiles, { return new member; })) { 473 enum isTestClass = false; //can't new it, can't use it 474 } else { 475 enum hasUnitTest = HasAttribute!(module_, moduleMember, UnitTest); 476 enum hasTestMethod = __traits(hasMember, member, "test"); 477 478 enum isTestClass = is(member == class) && (hasTestMethod || hasUnitTest); 479 } 480 } 481 482 return moduleTestData!(module_, isTestClass, memberTestData); 483 } 484 485 /** 486 * Finds all test functions in the given module. 487 * Returns an array of TestData structs 488 */ 489 TestData[] moduleTestFunctions(alias module_)() pure { 490 491 import unit_threaded.uda : isTypesAttr; 492 493 template isTestFunction(alias module_, string moduleMember) { 494 import unit_threaded.meta : importMember; 495 import unit_threaded.attrs : UnitTest; 496 import unit_threaded.uda : HasAttribute, GetTypes; 497 import std.meta : AliasSeq; 498 import std.traits : isSomeFunction; 499 500 alias member = Identity!(__traits(getMember, module_, moduleMember)); 501 502 static if (.isPrivate!(module_, moduleMember)) { 503 enum isTestFunction = false; 504 } else static if (AliasSeq!(member).length != 1) { 505 enum isTestFunction = false; 506 } else static if (isSomeFunction!member) { 507 enum isTestFunction = hasTestPrefix!(module_, moduleMember) 508 || HasAttribute!(module_, moduleMember, UnitTest); 509 } else static if (__traits(compiles, __traits(getAttributes, member))) { 510 // in this case we handle the possibility of a template function with 511 // the @Types UDA attached to it 512 alias types = GetTypes!member; 513 enum isTestFunction = hasTestPrefix!(module_, moduleMember) && types.length > 0; 514 } else { 515 enum isTestFunction = false; 516 } 517 518 } 519 520 template hasTestPrefix(alias module_, string memberName) { 521 import std.uni : isUpper; 522 import unit_threaded.meta : importMember; 523 524 alias member = Identity!(__traits(getMember, module_, memberName)); 525 526 enum prefix = "test"; 527 enum minSize = prefix.length + 1; 528 529 static if (memberName.length >= minSize 530 && memberName[0 .. prefix.length] == prefix && isUpper(memberName[prefix.length])) { 531 enum hasTestPrefix = true; 532 } else { 533 enum hasTestPrefix = false; 534 } 535 } 536 537 return moduleTestData!(module_, isTestFunction, createFuncTestData); 538 } 539 540 private TestData[] createFuncTestData(alias module_, string moduleMember)() { 541 import unit_threaded.meta : importMember; 542 import unit_threaded.uda : GetAttributes, HasAttribute, GetTypes, HasTypes; 543 import unit_threaded.attrs; 544 import std.meta : aliasSeqOf; 545 546 mixin(importMember!module_(moduleMember)); 547 /* 548 Get all the test functions for this module member. There might be more than one 549 when using parametrized unit tests. 550 551 Examples: 552 ------ 553 void testFoo() {} // -> the array contains one element, testFoo 554 @(1, 2, 3) void testBar(int) {} // The array contains 3 elements, one for each UDA value 555 @Types!(int, float) void testBaz(T)() {} //The array contains 2 elements, one for each type 556 ------ 557 */ 558 // if the predicate returned true (which is always the case here), then it's either 559 // a regular function or a templated one. If regular we can get a pointer to it 560 enum isRegularFunction = __traits(compiles, &__traits(getMember, module_, moduleMember)); 561 562 static if (isRegularFunction) { 563 564 enum func = &__traits(getMember, module_, moduleMember); 565 enum arity = arity!func; 566 567 static if (arity == 0) // the reason we're creating a lambda to call the function is that test functions 568 // are ordinary functions, but we're storing delegates 569 return [memberTestData!(module_, moduleMember)(() { func(); })]; //simple case, just call the function 570 else { 571 572 // the function has parameters, check if it has UDAs for value parameters to be passed to it 573 alias params = Parameters!func; 574 575 import std.range : iota; 576 import std.algorithm : any; 577 import std.typecons : tuple, Tuple; 578 579 bool hasAttributesForAllParams() { 580 auto ret = true; 581 foreach (p; params) { 582 if (tuple(GetAttributes!(module_, moduleMember, p)).length == 0) { 583 ret = false; 584 } 585 } 586 return ret; 587 } 588 589 static if (!hasAttributesForAllParams) { 590 import std.conv : text; 591 592 pragma(msg, text("Warning: ", moduleMember, 593 " passes the criteria for a value-parameterized test function", 594 " but doesn't have the appropriate value UDAs.\n", 595 " Consider changing its name or annotating it with @DontTest")); 596 return []; 597 } else { 598 599 static if (arity == 1) { 600 // bind a range of tuples to prod just as cartesianProduct returns 601 enum prod = [GetAttributes!(module_, moduleMember, params[0])].map!( 602 a => tuple(a)); 603 } else { 604 import std.conv : text; 605 606 mixin(`enum prod = cartesianProduct(` ~ params.length.iota.map!( 607 a => `[GetAttributes!(module_, moduleMember, params[` ~ guaranteedToString(a) ~ `])]`) 608 .join(", ") ~ `);`); 609 } 610 611 TestData[] testData; 612 foreach (comb; aliasSeqOf!prod) { 613 enum valuesName = valuesName(comb); 614 615 static if (HasAttribute!(module_, moduleMember, AutoTags)) 616 enum extraTags = valuesName.split(".").array; 617 else 618 enum string[] extraTags = []; 619 620 testData ~= memberTestData!(module_, moduleMember, extraTags)( // func(value0, value1, ...) 621 () { func(comb.expand); }, valuesName); 622 } 623 624 return testData; 625 } 626 } 627 } else static if (HasTypes!(mixin(moduleMember))) { //template function with @Types 628 alias types = GetTypes!(mixin(moduleMember)); 629 TestData[] testData; 630 foreach (type; types) { 631 632 static if (HasAttribute!(module_, moduleMember, AutoTags)) 633 enum extraTags = [type.stringof]; 634 else 635 enum string[] extraTags = []; 636 637 alias member = Identity!(mixin(moduleMember)); 638 639 testData ~= memberTestData!(module_, moduleMember, extraTags)(() { 640 member!type(); 641 }, type.stringof); 642 } 643 return testData; 644 } else { 645 return []; 646 } 647 } 648 649 // this funtion returns TestData for either classes or test functions 650 // built-in unittest modules are handled by moduleUnitTests 651 // pred determines what qualifies as a test 652 // createTestData must return TestData[] 653 private TestData[] moduleTestData(alias module_, alias pred, alias createTestData)() pure { 654 import std.traits : fullyQualifiedName; 655 656 mixin("import " ~ fullyQualifiedName!module_ ~ ";"); //so it's visible 657 658 TestData[] testData; 659 660 foreach (moduleMember; __traits(allMembers, module_)) { 661 662 static if (PassesTestPred!(module_, pred, moduleMember)) 663 testData ~= createTestData!(module_, moduleMember); 664 } 665 666 return testData; 667 668 } 669 670 // TestData for a member of a module (either a test function or test class) 671 private TestData memberTestData(alias module_, string moduleMember, string[] extraTags = [])( 672 TestFunction testFunction = null, string suffix = "") { 673 674 import unit_threaded.uda : HasAttribute, GetAttributes, hasUtUDA; 675 import unit_threaded.attrs; 676 import std.traits : fullyQualifiedName; 677 678 mixin("import " ~ fullyQualifiedName!module_ ~ ";"); //so it's visible 679 680 immutable singleThreaded = HasAttribute!(module_, moduleMember, Serial); 681 enum builtin = false; 682 enum tags = tagsFromAttrs!(GetAttributes!(module_, moduleMember, Tags)); 683 enum exceptionTypeInfo = getExceptionTypeInfo!(mixin(moduleMember)); 684 enum shouldFail = HasAttribute!(module_, moduleMember, ShouldFail) 685 || hasUtUDA!(mixin(moduleMember), ShouldFailWith); 686 enum flakyRetries = getFlakyRetries!(mixin(moduleMember)); 687 688 return TestData(fullyQualifiedName!module_ ~ "." ~ moduleMember, testFunction, HasAttribute!(module_, moduleMember, 689 HiddenTest), shouldFail, singleThreaded, builtin, suffix, 690 tags ~ extraTags, exceptionTypeInfo, flakyRetries); 691 } 692 693 private int getFlakyRetries(alias test)() { 694 import unit_threaded.attrs : Flaky; 695 import std.traits : getUDAs; 696 import std.conv : text; 697 698 alias flakies = getUDAs!(test, Flaky); 699 700 static assert(flakies.length == 0 || flakies.length == 1, text("Only 1 @Flaky allowed, found ", 701 flakies.length, " on ", __traits(identifier, test))); 702 703 static if (flakies.length == 1) { 704 static if (is(flakies[0])) 705 return Flaky.defaultRetries; 706 else 707 return flakies[0].retries; 708 } else 709 return 0; 710 } 711 712 string[] tagsFromAttrs(T...)() { 713 static assert(T.length <= 1, "@Tags can only be applied once"); 714 static if (T.length) 715 return T[0].values; 716 else 717 return []; 718 }